using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Castle.Services.Transaction;
using DecisionMaking.Domain;
using log4net;
using NHibernate;

namespace DecisionMaking.BusinessLogic.Impl
{
    [Transactional]
    public class ExecutionManager : IExecutionManager
    {
        private const int MaxThreads = 5;
        private static readonly ILog Log = LogManager.GetLogger(typeof(ExecutionManager));

        public IEnvironmentManager EnvironmentManager {private get; set;} // injected by Windsor
        public IAlgorithmManager AlgorithmManager {private get; set;} // injected by Windsor

        /// <summary>
        /// Holds a reference of execution id to the execution context
        /// </summary>
        private IDictionary<Guid, ExecutionContext> _executionContexts = new Dictionary<Guid, ExecutionContext>();

        public AlgorithmExecutionOutcome ExecuteAgent(IAlgorithm algorithm, Agent agent, IList<Candidate> candidates, IList<Cue> cues)
        {
            Log.Debug("Executing simulation for agent " + agent.Id);

            var totalRuntime = 0.0;
            var totalCuesUsed = 0.0;
            var totalCorrects = 0;
            var totalExecutions = candidates.Count * (candidates.Count - 1) / 2;
            var random = new Random();

            // iterate over all possible pairs of canidadates
            for (var i = 0; i < candidates.Count; i++)
            {
                for (var j = i + 1; j < candidates.Count; j++)
                {
                    var left = candidates[i];
                    var right = candidates[j];

                    var before = DateTime.Now; // measure execution time
                    var result = algorithm.Execute(agent, cues, left, right);
                    var duration = DateTime.Now - before;

                    // accumulate counters
                    totalRuntime += duration.TotalMilliseconds;
                    totalCuesUsed += result.CuesUsed;

                    if ((result.Result < 0 && left.IndependentVariableValue > right.IndependentVariableValue)
                        || (result.Result > 0 && right.IndependentVariableValue > left.IndependentVariableValue)
                        || (result.Result == 0 && random.Next(0, 2) == 1)) // guess
                    {
                        totalCorrects++;
                    }
                }
            }

            var outcome = new AlgorithmExecutionOutcome
            {
                Agent = agent,
                AlgorithmDefinition = algorithm.GetDefinition(),
                TotalComparisons = totalExecutions,
                NumOfCorrectInferences = totalCorrects,
                AvgRuntimeInMillis = totalRuntime / totalExecutions,
                AvgCuesUsed = totalCuesUsed / totalExecutions,
                AvgAccuracy = (double)totalCorrects / totalExecutions,
                NumOfCandidatesRecognized = agent.CandidatesRecognized.Count
            };

            return outcome;
        }

        public Guid ExecuteEnvironment(int algorithmDefinitionId, int environmentId)
        {
            var algorithmDefinition = AlgorithmManager.GetAlgorithmDefinition(algorithmDefinitionId);
            var algorithm = AlgorithmManager.GetAlgorithmInstance(algorithmDefinition);
            var environment = EnvironmentManager.GetEnvironment(environmentId);

            Log.Debug(string.Format("Running simulation on Environment {0} using algorithm {1}", environment.Name, algorithmDefinition.Name));

            // prepare simulation outcome
            var outcome = new SimulationOutcome
            {
                Id = Guid.NewGuid(),
                Environment = environment,
                AlgorithmDefinition = algorithmDefinition,
                ExecutionDate = DateTime.Now                
            };

            // load agents and candidates
            var agents = EnvironmentManager.GetAgents(environmentId);
            var candidates = EnvironmentManager.GetCandidates(environmentId);
            var cues = EnvironmentManager.GetCues(environmentId);

            // create a context for the specified execution
            var context = new ExecutionContext(this, outcome);

            context.EnqueueAll(algorithm, agents, candidates, cues);

            _executionContexts.Add(outcome.Id, context);

            return outcome.Id;
        }

        [Transaction(TransactionMode.Supported)]
        public SimulationOutcome GetSimulationOutcome(Guid id)
        {
            try
            {
                return SimulationOutcome.Find(id);
            }
            catch (ObjectNotFoundException e)
            {
                throw new EntityNotFoundException(typeof(SimulationOutcome), id, e);
            }
        }

         [Transaction(TransactionMode.Supported)]
        public IList<SimulationOutcome> GetSimulationOutcomes(int accountId)
        {
             return SimulationOutcome.FindByAccountId(accountId);
        }

        public double? GetExecutionProgress(Guid id)
        {
            if (_executionContexts.ContainsKey(id))
            {
                return _executionContexts[id].PercentDone;
            }
            else
            {
                return null;
            }
        }

        public AlgorithmExecutionException GetLastException(Guid id)
        {
            if (_executionContexts.ContainsKey(id))
            {
                var e = _executionContexts[id].LastException;

                if (e != null) // if there was an exception, kill the execution context
                {
                    _executionContexts.Remove(id);
                }

                return e;
            }
            else
            {
                return null;
            }
        }

        [Transaction(TransactionMode.Requires)]
        internal void FinishExecution(ExecutionContext context)
        {
            var outcome = context.Outcome;

            Log.Debug("Saving simulation outcome " + outcome.Id);

            // calculate averages
            outcome.AverageRunTime = outcome.ExecutionOutcomes.Average(e => e.AvgRuntimeInMillis);
            outcome.AverageCuesUsed = outcome.ExecutionOutcomes.Average(e => e.AvgCuesUsed);
            outcome.AverageCandidatesRecognized = outcome.ExecutionOutcomes.Average(e => e.NumOfCandidatesRecognized);
            outcome.AverageAccuracy = outcome.ExecutionOutcomes.Average(e => e.AvgAccuracy);
            outcome.Create();

            _executionContexts.Remove(outcome.Id);
        }

    }

    /// <summary>
    /// Encapsulates parallel execution and reporting of execution completion
    /// </summary>
    internal class ExecutionContext
    {
        /// <summary>
        /// Encapsulates parameters for executing a simulation on a specific agent
        /// </summary>
        private struct AgentExecutionRequest
        {
            public IAlgorithm Algorithm;
            public Agent Agent;
            public IList<Candidate> Candidates;
            public IList<Cue> Cues;

            public AgentExecutionRequest(IAlgorithm algorithm, Agent agent, IList<Candidate> candidates, IList<Cue> cues)
            {
                Algorithm = algorithm;
                Agent = agent;
                Candidates = candidates;
                Cues = cues;
            }
        }

        private readonly ExecutionManager _executionManager;
        private readonly SimulationOutcome _simulationOutcome;
        private int _completedThreads = 0;
        private int _totalThreads = 0;
        private bool _running = false;
        private AlgorithmExecutionException _lastException;

        private static readonly ILog Log = LogManager.GetLogger(typeof(ExecutionContext));

        /// <summary>
        /// Used for executing several agent simulations in parellel
        /// </summary>
        /// <param name="request"></param>
        private delegate void AgentExecutingDelegate(AgentExecutionRequest request);

        public ExecutionContext(ExecutionManager executionManager, SimulationOutcome simulationOutcome)
        {
            _executionManager = executionManager;
            _simulationOutcome = simulationOutcome;
        }

        /// <summary>
        /// The outcome of this execution context
        /// </summary>
        public SimulationOutcome Outcome { get { return _simulationOutcome; } }

        /// <summary>
        /// An indication of how many threads have completed
        /// </summary>
        /// <returns></returns>
        public double PercentDone { get { return (double)_completedThreads / _totalThreads; } }

        /// <summary>
        /// The last exception that occured in this context
        /// </summary>
        public AlgorithmExecutionException LastException
        {
            get { return _lastException; }
        }


        /// <summary>
        /// Enqueues all agents to be executed using the specified algorithm
        /// </summary>
        /// <param name="algorithm"></param>
        /// <param name="agents"></param>
        /// <param name="candidates"></param>
        /// <param name="cues"></param>
        public void EnqueueAll(IAlgorithm algorithm, IList<Agent> agents, IList<Candidate> candidates, IList<Cue> cues)
        {
            _running = true;

            Interlocked.Exchange(ref _totalThreads, agents.Count); // set number of total threads

            foreach (var agent in agents)
            {
                Log.Debug(string.Format("Enqueing execution for agent {0}", agent.Id));
                new AgentExecutingDelegate(ExecuteRequest).BeginInvoke(new AgentExecutionRequest(algorithm, agent, candidates, cues), null, null);
            }
        }

        /// <summary>
        /// Executes a single request for Agent simulation, updating the outcome list and completion count 
        /// </summary>
        /// <param name="request"></param>
        private void ExecuteRequest(AgentExecutionRequest request)
        {
            if (_running)
            {
                Log.Debug(string.Format("Execution for agent {0} has been dequeued", request.Agent.Id));

                AlgorithmExecutionOutcome outcome;
                try
                {
                    outcome = _executionManager.ExecuteAgent(request.Algorithm, request.Agent, request.Candidates,
                                                                 request.Cues);
                }
                catch (AlgorithmExecutionException e)
                {
                    HandlerError(e);
                    return;
                }

                Interlocked.Increment(ref _completedThreads);

                Log.Debug(string.Format("Execution for agent {0} returned; adding to outcome list.", request.Agent.Id));

                lock (_simulationOutcome)
                {
                    _simulationOutcome.AddExecutionOutcome(outcome);

                    if (_completedThreads == _totalThreads)
                    {
                        // if we got here, all threads have finished executing, save outcome and purge this context
                        _executionManager.FinishExecution(this);
                    }
                }

                Log.Debug(string.Format("Execution for agent {0} is done; agents in queue: {1}", request.Agent.Id,
                                        _totalThreads - _completedThreads));
            }
        }

        private void HandlerError(AlgorithmExecutionException e)
        {
            _running = false;
            _lastException = e;
        }
    }

}